// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Xml;

namespace RazorSyntaxGenerator;

internal class SourceWriter : AbstractFileWriter
{
    private SourceWriter(TextWriter writer, Tree tree)
        : base(writer, tree)
    {
    }

    public static void WriteMain(TextWriter writer, Tree tree) => new SourceWriter(writer, tree).WriteMain();

    public static void WriteInternal(TextWriter writer, Tree tree) => new SourceWriter(writer, tree).WriteInternal();

    public static void WriteSyntax(TextWriter writer, Tree tree) => new SourceWriter(writer, tree).WriteSyntax();

    private void WriteFileHeader()
    {
        WriteLine("// <auto-generated />");
        WriteLine();
        WriteLine("using System;");
        WriteLine("using System.Collections;");
        WriteLine("using System.Collections.Generic;");
        WriteLine("using System.Linq;");
        WriteLine("using System.Threading;");
        WriteLine();
    }

    private void WriteInternal()
    {
        WriteFileHeader();

        WriteLine("namespace Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax");
        WriteLine("{");
        WriteLine();
        WriteGreenTypes();
        WriteGreenVisitors();
        WriteGreenRewriter();
        WriteStaticGreenFactories();
        WriteLine("}");
    }

    private void WriteSyntax()
    {
        WriteFileHeader();

        WriteLine();
        WriteLine("namespace Microsoft.AspNetCore.Razor.Language.Syntax");
        WriteLine("{");
        WriteLine();
        WriteRedTypes();
        WriteLine("}");
    }

    private void WriteMain()
    {
        WriteFileHeader();

        WriteLine();
        WriteLine("namespace Microsoft.AspNetCore.Razor.Language.Syntax");
        WriteLine("{");
        //WriteLine("    using Microsoft.AspNetCore.Razor.Language.Syntax;");
        //WriteLine();
        WriteRedVisitors();
        WriteRedRewriter();
        WriteRedFactories();
        WriteLine("}");
    }

    private void WriteGreenTypes()
    {
        var nodes = Tree.Types.Where(n => !(n is PredefinedNode)).ToList();
        for (int i = 0, n = nodes.Count; i < n; i++)
        {
            var node = nodes[i];
            WriteLine();
            WriteGreenType(node);
        }
    }

    private void WriteGreenType(TreeType node)
    {
        WriteComment(node.TypeComment, "  ");

        if (node is AbstractNode)
        {
            AbstractNode nd = (AbstractNode)node;
            WriteLine("  internal abstract partial class {0} : {1}", node.Name, node.Base == "SyntaxNode" ? "GreenNode" : node.Base);
            WriteLine("  {");

            // ctor with diagnostics and annotations
            WriteLine("    internal {0}(SyntaxKind kind, RazorDiagnostic[] diagnostics, SyntaxAnnotation[] annotations)", node.Name);
            WriteLine("      : base(kind, diagnostics, annotations)");
            WriteLine("    {");
            if (node.Name == "DirectiveTriviaSyntax")
            {
                WriteLine("      _flags |= NodeFlags.ContainsDirectives;");
            }
            WriteLine("    }");

            // ctor without diagnostics and annotations
            WriteLine("    internal {0}(SyntaxKind kind)", node.Name);
            WriteLine("      : base(kind)");
            WriteLine("    {");
            if (node.Name == "DirectiveTriviaSyntax")
            {
                WriteLine("      _flags |= NodeFlags.ContainsDirectives;");
            }
            WriteLine("    }");

            /* Remove
            // object reader constructor
            WriteLine();
            WriteLine("    protected {0}(ObjectReader reader)", node.Name);
            WriteLine("       : base(reader)");
            WriteLine("    {");
            if (node.Name == "DirectiveTriviaSyntax")
            {
                WriteLine("      _flags |= NodeFlags.ContainsDirectives;");
            }
            WriteLine("    }"); */

            var valueFields = nd.Fields.Where(n => !IsNodeOrNodeList(n.Type)).ToList();
            var nodeFields = nd.Fields.Where(n => IsNodeOrNodeList(n.Type)).ToList();

            for (int i = 0, n = nodeFields.Count; i < n; i++)
            {
                var field = nodeFields[i];
                if (IsNodeOrNodeList(field.Type))
                {
                    WriteLine();
                    WriteComment(field.PropertyComment, "    ");

                    if (IsSeparatedNodeList(field.Type) ||
                        IsNodeList(field.Type))
                    {
                        WriteLine("    public abstract {0}{1} {2} {{ get; }}",
                            (IsNew(field) ? "new " : ""), field.Type, field.Name);
                    }
                    else
                    {
                        WriteLine("    public abstract {0}{1} {2} {{ get; }}",
                            (IsNew(field) ? "new " : ""), field.Type, field.Name);
                    }
                }
            }

            for (int i = 0, n = valueFields.Count; i < n; i++)
            {
                var field = valueFields[i];
                WriteLine();
                WriteComment(field.PropertyComment, "    ");

                WriteLine("   public abstract {0}{1} {2} {{ get; }}",
                    (IsNew(field) ? "new " : ""), field.Type, field.Name);
            }

            WriteLine("  }");
        }
        else if (node is Node)
        {
            Node nd = (Node)node;

            WriteLine("  internal sealed partial class {0} : {1}", node.Name, node.Base);
            WriteLine("  {");

            var valueFields = nd.Fields.Where(n => !IsNodeOrNodeList(n.Type)).ToList();
            var nodeFields = nd.Fields.Where(n => IsNodeOrNodeList(n.Type)).ToList();

            for (int i = 0, n = nodeFields.Count; i < n; i++)
            {
                var field = nodeFields[i];
                var type = GetFieldType(field, green: true);
                WriteLine("    private readonly {0} {1};", type, UnderscoreCamelCase(field.Name));
            }

            for (int i = 0, n = valueFields.Count; i < n; i++)
            {
                var field = valueFields[i];
                WriteLine("    private readonly {0} {1};", field.Type, UnderscoreCamelCase(field.Name));
            }

            // write constructor with diagnostics and annotations
            WriteLine();
            Write("    internal {0}(SyntaxKind kind", node.Name);

            WriteGreenNodeConstructorArgs(nodeFields, valueFields);

            WriteLine(", RazorDiagnostic[] diagnostics, SyntaxAnnotation[] annotations)");
            WriteLine("        : base(kind, diagnostics, annotations)");
            WriteLine("    {");
            WriteCtorBody(valueFields, nodeFields);
            WriteLine("    }");
            WriteLine();

            /* Remove
            // write constructor with async
            WriteLine();
            Write("    internal {0}(SyntaxKind kind", node.Name);

            WriteGreenNodeConstructorArgs(nodeFields, valueFields);

            WriteLine(", SyntaxFactoryContext context)");
            WriteLine("        : base(kind)");
            WriteLine("    {");
            WriteLine("        this.SetFactoryContext(context);");
            WriteCtorBody(valueFields, nodeFields);
            WriteLine("    }");
            WriteLine(); */

            // write constructor without diagnostics and annotations
            WriteLine();
            Write("    internal {0}(SyntaxKind kind", node.Name);

            WriteGreenNodeConstructorArgs(nodeFields, valueFields);

            WriteLine(")");
            WriteLine("        : base(kind)");
            WriteLine("    {");
            WriteCtorBody(valueFields, nodeFields);
            WriteLine("    }");
            WriteLine();

            // property accessors
            for (int i = 0, n = nodeFields.Count; i < n; i++)
            {
                var field = nodeFields[i];
                WriteComment(field.PropertyComment, "    ");
                if (IsNodeList(field.Type))
                {
                    WriteLine("    public {0}{1} {2} {{ get {{ return new {1}({3}); }} }}",
                        OverrideOrNewModifier(field), field.Type, field.Name, UnderscoreCamelCase(field.Name)
                        );
                }
                else if (IsSeparatedNodeList(field.Type))
                {
                    WriteLine("    public {0}{1} {2} {{ get {{ return new {1}(new SyntaxList<GreenNode>({3})); }} }}",
                        OverrideOrNewModifier(field), field.Type, field.Name, UnderscoreCamelCase(field.Name), i
                        );
                }
                else if (field.Type == "SyntaxNodeOrTokenList")
                {
                    WriteLine("    public {0}SyntaxList<GreenNode> {1} {{ get {{ return new SyntaxList<GreenNode>({2}); }} }}",
                        OverrideOrNewModifier(field), field.Name, UnderscoreCamelCase(field.Name)
                        );
                }
                else
                {
                    WriteLine("    public {0}{1} {2} {{ get {{ return {3}; }} }}",
                        OverrideOrNewModifier(field), field.Type, field.Name, UnderscoreCamelCase(field.Name)
                        );
                }
            }

            for (int i = 0, n = valueFields.Count; i < n; i++)
            {
                var field = valueFields[i];
                WriteComment(field.PropertyComment, "    ");
                WriteLine("    public {0}{1} {2} {{ get {{ return {3}; }} }}",
                    OverrideOrNewModifier(field), field.Type, field.Name, UnderscoreCamelCase(field.Name)
                    );
            }

            // GetSlot
            WriteLine();
            WriteLine("    internal override GreenNode GetSlot(int index)");
            WriteLine("    {");
            WriteLine("        switch (index)");
            WriteLine("        {");
            for (int i = 0, n = nodeFields.Count; i < n; i++)
            {
                var field = nodeFields[i];
                WriteLine("            case {0}: return {1};", i, UnderscoreCamelCase(field.Name));
            }
            WriteLine("            default: return null;");
            WriteLine("        }");
            WriteLine("    }");

            WriteLine();
            WriteLine("    internal override SyntaxNode CreateRed(SyntaxNode parent, int position)");
            WriteLine("    {");
            WriteLine("      return new Syntax.{0}(this, parent, position);", node.Name);
            WriteLine("    }");

            WriteGreenAcceptMethods(nd);
            WriteGreenUpdateMethod(nd);
            WriteSetDiagnostics(nd);
            WriteSetAnnotations(nd);

            WriteLine("  }");
        }
    }

    private void WriteGreenNodeConstructorArgs(List<Field> nodeFields, List<Field> valueFields)
    {
        for (int i = 0, n = nodeFields.Count; i < n; i++)
        {
            var field = nodeFields[i];
            string type = GetFieldType(field, green: true);

            Write(", {0} {1}", type, CamelCase(field.Name));
        }

        for (int i = 0, n = valueFields.Count; i < n; i++)
        {
            var field = valueFields[i];
            Write(", {0} {1}", field.Type, CamelCase(field.Name));
        }
    }

    private void WriteGreenSerialization(Node node)
    {
        var valueFields = node.Fields.Where(n => !IsNodeOrNodeList(n.Type)).ToList();
        var nodeFields = node.Fields.Where(n => IsNodeOrNodeList(n.Type)).ToList();

        // object reader constructor
        WriteLine();
        WriteLine("    internal {0}(ObjectReader reader)", node.Name);
        WriteLine("        : base(reader)");
        WriteLine("    {");

        WriteLine("      this.SlotCount = {0};", nodeFields.Count);

        for (int i = 0, n = nodeFields.Count; i < n; i++)
        {
            var field = nodeFields[i];
            string type = GetFieldType(field, green: true);
            WriteLine("      var {0} = ({1})reader.ReadValue();", CamelCase(field.Name), type);
            WriteLine("      if ({0} != null)", CamelCase(field.Name));
            WriteLine("      {");
            WriteLine("         AdjustFlagsAndWidth({0});", CamelCase(field.Name));
            WriteLine("         this.{0} = {0};", CamelCase(field.Name), type);
            WriteLine("      }");
        }

        for (int i = 0, n = valueFields.Count; i < n; i++)
        {
            var field = valueFields[i];
            string type = GetFieldType(field, green: true);
            WriteLine("      this.{0} = ({1})reader.{2}();", CamelCase(field.Name), type, GetReaderMethod(type));
        }

        WriteLine("    }");

        // IWritable
        WriteLine();
        WriteLine("    internal override void WriteTo(ObjectWriter writer)");
        WriteLine("    {");
        WriteLine("      base.WriteTo(writer);");

        for (int i = 0, n = nodeFields.Count; i < n; i++)
        {
            var field = nodeFields[i];
            string type = GetFieldType(field, green: true);
            WriteLine("      writer.WriteValue(this.{0});", CamelCase(field.Name));
        }

        for (int i = 0, n = valueFields.Count; i < n; i++)
        {
            var field = valueFields[i];
            var type = GetFieldType(field, green: true);
            WriteLine("      writer.{0}(this.{1});", GetWriterMethod(type), CamelCase(field.Name));
        }

        WriteLine("    }");

        // IReadable
        WriteLine();
        WriteLine("    static {0}()", node.Name);
        WriteLine("    {");
        WriteLine("       ObjectBinder.RegisterTypeReader(typeof({0}), r => new {0}(r));", node.Name);
        WriteLine("    }");
    }

    private string GetWriterMethod(string type)
    {
        switch (type)
        {
            case "bool":
                return "WriteBoolean";
            default:
                throw new InvalidOperationException("Type 'type' not supported for object reader serialization.");
        }
    }

    private string GetReaderMethod(string type)
    {
        switch (type)
        {
            case "bool":
                return "ReadBoolean";
            default:
                throw new InvalidOperationException("Type 'type' not supported for object reader serialization.");
        }
    }

    private void WriteCtorBody(List<Field> valueFields, List<Field> nodeFields)
    {
        // constructor body
        WriteLine("        SlotCount = {0};", nodeFields.Count);

        for (int i = 0, n = nodeFields.Count; i < n; i++)
        {
            var field = nodeFields[i];
            if (IsAnyList(field.Type) || IsOptional(field))
            {
                WriteLine("        if ({0} != null)", CamelCase(field.Name));
                WriteLine("        {");
                WriteLine("            AdjustFlagsAndWidth({0});", CamelCase(field.Name));
                WriteLine("            {0} = {1};", UnderscoreCamelCase(field.Name), CamelCase(field.Name));
                WriteLine("        }");
            }
            else
            {
                WriteLine("        AdjustFlagsAndWidth({0});", CamelCase(field.Name));
                WriteLine("        {0} = {1};", UnderscoreCamelCase(field.Name), CamelCase(field.Name));
            }
        }

        for (int i = 0, n = valueFields.Count; i < n; i++)
        {
            var field = valueFields[i];
            WriteLine("        {0} = {1};", UnderscoreCamelCase(field.Name), CamelCase(field.Name));
        }
    }

    private void WriteSetAnnotations(Node node)
    {
        WriteLine();
        WriteLine("    internal override GreenNode SetAnnotations(SyntaxAnnotation[] annotations)");
        WriteLine("    {");

        Write("         return new {0}(", node.Name);
        Write("Kind, ");
        for (int f = 0; f < node.Fields.Count; f++)
        {
            var field = node.Fields[f];
            if (f > 0)
                Write(", ");
            Write("{0}", UnderscoreCamelCase(field.Name));
        }
        WriteLine(", GetDiagnostics(), annotations);");
        WriteLine("    }");
    }

    private void WriteSetDiagnostics(Node node)
    {
        WriteLine();
        WriteLine("    internal override GreenNode SetDiagnostics(RazorDiagnostic[] diagnostics)");
        WriteLine("    {");

        Write("         return new {0}(", node.Name);
        Write("Kind, ");
        for (int f = 0; f < node.Fields.Count; f++)
        {
            var field = node.Fields[f];
            if (f > 0)
                Write(", ");
            Write("{0}", UnderscoreCamelCase(field.Name));
        }
        WriteLine(", diagnostics, GetAnnotations());");
        WriteLine("    }");
    }

    private void WriteGreenAcceptMethods(Node node)
    {
        //WriteLine();
        //WriteLine("    public override TResult Accept<TArgument, TResult>(SyntaxVisitor<TArgument, TResult> visitor, TArgument argument)");
        //WriteLine("    {");
        //WriteLine("        return visitor.Visit{0}(this, argument);", StripPost(node.Name, "Syntax"));
        //WriteLine("    }");
        WriteLine();
        WriteLine("    public override TResult Accept<TResult>(SyntaxVisitor<TResult> visitor)");
        WriteLine("    {");
        WriteLine("        return visitor.Visit{0}(this);", StripPost(node.Name, "Syntax"));
        WriteLine("    }");
        WriteLine();
        WriteLine("    public override void Accept(SyntaxVisitor visitor)");
        WriteLine("    {");
        WriteLine("        visitor.Visit{0}(this);", StripPost(node.Name, "Syntax"));
        WriteLine("    }");
    }

    private void WriteGreenVisitors()
    {
        //WriteGreenVisitor(true, true);
        //WriteLine();
        WriteGreenVisitor(false, true);
        WriteLine();
        WriteGreenVisitor(false, false);
    }

    private void WriteGreenVisitor(bool withArgument, bool withResult)
    {
        var nodes = Tree.Types.Where(n => !(n is PredefinedNode)).ToList();

        WriteLine();
        WriteLine("  internal partial class SyntaxVisitor" + (withResult ? "<" + (withArgument ? "TArgument, " : "") + "TResult>" : ""));
        WriteLine("  {");
        int nWritten = 0;
        for (int i = 0, n = nodes.Count; i < n; i++)
        {
            if (nodes[i] is Node node)
            {
                if (nWritten > 0)
                    WriteLine();
                nWritten++;
                WriteLine("    public virtual " + (withResult ? "TResult" : "void") + " Visit{0}({1} node{2})", StripPost(node.Name, "Syntax"), node.Name, withArgument ? ", TArgument argument" : "");
                WriteLine("    {");
                WriteLine("      " + (withResult ? "return " : "") + "DefaultVisit(node{0});", withArgument ? ", argument" : "");
                WriteLine("    }");
            }
        }
        WriteLine("  }");
    }

    private void WriteGreenUpdateMethod(Node node)
    {
        WriteLine();
        Write("    public {0} Update(", node.Name);

        // parameters
        for (int f = 0; f < node.Fields.Count; f++)
        {
            var field = node.Fields[f];
            if (f > 0)
                Write(", ");

            var type =
                field.Type == "SyntaxNodeOrTokenList" ? "Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax.SyntaxList<GreenNode>" :
                field.Type == "SyntaxTokenList" ? "Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax.SyntaxList<SyntaxToken>" :
                IsNodeList(field.Type) ? "Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax." + field.Type :
                IsSeparatedNodeList(field.Type) ? "Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax." + field.Type :
                field.Type;

            Write("{0} {1}", type, CamelCase(field.Name));
        }
        WriteLine(")");
        WriteLine("    {");

        Write("        if (");
        int nCompared = 0;
        for (int f = 0; f < node.Fields.Count; f++)
        {
            var field = node.Fields[f];
            if (IsDerivedOrListOfDerived("SyntaxNode", field.Type) || IsDerivedOrListOfDerived("SyntaxToken", field.Type) || field.Type == "SyntaxNodeOrTokenList")
            {
                if (nCompared > 0)
                    Write(" || ");
                Write("{0} != {1}", CamelCase(field.Name), field.Name);
                nCompared++;
            }
        }
        if (nCompared > 0)
        {
            WriteLine(")");
            WriteLine("        {");
            Write("            var newNode = SyntaxFactory.{0}(", StripPost(node.Name, "Syntax"));
            if (node.Kinds.Count > 1)
            {
                Write("Kind, ");
            }
            for (int f = 0; f < node.Fields.Count; f++)
            {
                var field = node.Fields[f];
                if (f > 0)
                    Write(", ");
                Write(CamelCase(field.Name));
            }
            WriteLine(");");
            WriteLine("            var diags = GetDiagnostics();");
            WriteLine("            if (diags != null && diags.Length > 0)");
            WriteLine("               newNode = newNode.WithDiagnosticsGreen(diags);");
            WriteLine("            var annotations = GetAnnotations();");
            WriteLine("            if (annotations != null && annotations.Length > 0)");
            WriteLine("               newNode = newNode.WithAnnotationsGreen(annotations);");
            WriteLine("            return newNode;");
            WriteLine("        }");
        }

        WriteLine();
        WriteLine("        return this;");
        WriteLine("    }");
    }

    private void WriteGreenRewriter()
    {
        var nodes = Tree.Types.Where(n => !(n is PredefinedNode)).ToList();

        WriteLine();
        WriteLine("  internal partial class SyntaxRewriter : SyntaxVisitor<GreenNode>");
        WriteLine("  {");
        int nWritten = 0;
        for (int i = 0, n = nodes.Count; i < n; i++)
        {
            if (nodes[i] is Node node)
            {
                var nodeFields = node.Fields.Where(nd => IsNodeOrNodeList(nd.Type)).ToList();

                if (nWritten > 0)
                    WriteLine();
                nWritten++;
                WriteLine("    public override GreenNode Visit{0}({1} node)", StripPost(node.Name, "Syntax"), node.Name);
                WriteLine("    {");
                for (int f = 0; f < nodeFields.Count; f++)
                {
                    var field = nodeFields[f];
                    if (IsAnyList(field.Type))
                    {
                        WriteLine("      var {0} = VisitList(node.{1});", CamelCase(field.Name), field.Name);
                    }
                    else
                    {
                        WriteLine("      var {0} = ({1})Visit(node.{2});", CamelCase(field.Name), field.Type, field.Name);
                    }
                }
                if (nodeFields.Count > 0)
                {
                    Write("      return node.Update(");
                    for (int f = 0; f < node.Fields.Count; f++)
                    {
                        var field = node.Fields[f];
                        if (f > 0)
                            Write(", ");
                        if (IsNodeOrNodeList(field.Type))
                        {
                            Write(CamelCase(field.Name));
                        }
                        else
                        {
                            Write("node.{0}", field.Name);
                        }
                    }
                    WriteLine(");");
                }
                else
                {
                    WriteLine("      return node;");
                }
                WriteLine("    }");
            }
        }
        WriteLine("  }");
    }

    private void WriteContextualGreenFactories()
    {
        var nodes = Tree.Types.Where(n => !(n is PredefinedNode) && !(n is AbstractNode)).ToList();
        WriteLine();
        WriteLine("  internal partial class ContextAwareSyntax");
        WriteLine("  {");

        WriteLine();
        WriteLine("    private SyntaxFactoryContext context;");
        WriteLine();

        WriteLine();
        WriteLine("    public ContextAwareSyntax(SyntaxFactoryContext context)");
        WriteLine("    {");
        WriteLine("        this.context = context;");
        WriteLine("    }");

        WriteGreenFactories(nodes, withSyntaxFactoryContext: true);

        WriteLine("  }");
    }

    private void WriteStaticGreenFactories()
    {
        var nodes = Tree.Types.Where(n => !(n is PredefinedNode) && !(n is AbstractNode)).ToList();
        WriteLine();
        WriteLine("  internal static partial class SyntaxFactory");
        WriteLine("  {");

        WriteGreenFactories(nodes);

        WriteGreenTypeList();

        WriteLine("  }");
    }

    private void WriteGreenFactories(List<TreeType> nodes, bool withSyntaxFactoryContext = false)
    {
        for (int i = 0, n = nodes.Count; i < n; i++)
        {
            var node = nodes[i];
            WriteGreenFactory((Node)node, withSyntaxFactoryContext);
            if (i < n - 1)
                WriteLine();
        }
    }

    private void WriteGreenTypeList()
    {
        WriteLine();
        WriteLine("    internal static IEnumerable<Type> GetNodeTypes()");
        WriteLine("    {");
        WriteLine("        return new Type[] {");

        var nodes = Tree.Types.Where(n => !(n is PredefinedNode) && !(n is AbstractNode)).ToList();
        for (int i = 0, n = nodes.Count; i < n; i++)
        {
            var node = nodes[i];
            Write("           typeof({0})", node.Name);
            if (i < n - 1)
                Write(",");
            WriteLine();
        }

        WriteLine("        };");
        WriteLine("    }");
    }


    private void WriteGreenFactory(Node nd, bool withSyntaxFactoryContext = false)
    {
        var valueFields = nd.Fields.Where(n => !IsNodeOrNodeList(n.Type)).ToList();
        var nodeFields = nd.Fields.Where(n => IsNodeOrNodeList(n.Type)).ToList();

        Write("    public {0}{1} {2}(", withSyntaxFactoryContext ? "" : "static ", nd.Name, StripPost(nd.Name, "Syntax"));
        WriteGreenFactoryParameters(nd);
        WriteLine(")");
        WriteLine("    {");

        // validate kind
        if (nd.Kinds.Count > 1)
        {
            WriteLine("      switch (kind)");
            WriteLine("      {");
            foreach (var k in nd.Kinds)
            {
                WriteLine("        case SyntaxKind.{0}:", k.Name);
            }
            WriteLine("          break;");
            WriteLine("        default:");
            WriteLine("          throw new ArgumentException(\"kind\");");
            WriteLine("      }");
        }

        // validate parameters
        //WriteLine("#if DEBUG");
        for (int i = 0, n = nodeFields.Count; i < n; i++)
        {
            var field = nodeFields[i];
            var pname = CamelCase(field.Name);

            if (!IsAnyList(field.Type) && !IsOptional(field))
            {
                WriteLine("      if ({0} == null)", CamelCase(field.Name));
                WriteLine("        throw new ArgumentNullException(nameof({0}));", CamelCase(field.Name));
            }
            if (field.Type == "SyntaxToken" && field.Kinds != null && field.Kinds.Count > 0)
            {
                if (IsOptional(field))
                {
                    WriteLine("      if ({0} != null)", CamelCase(field.Name));
                    WriteLine("      {");
                }
                WriteLine("      switch ({0}.Kind)", pname);
                WriteLine("      {");
                foreach (var kind in field.Kinds)
                {
                    WriteLine("        case SyntaxKind.{0}:", kind.Name);
                }
                //we need to check for Kind=None as well as node == null because that's what the red factory will pass
                if (IsOptional(field))
                {
                    WriteLine("        case SyntaxKind.None:");
                }
                WriteLine("          break;");
                WriteLine("        default:");
                WriteLine("          throw new ArgumentException(\"{0}\");", pname);
                WriteLine("      }");
                if (IsOptional(field))
                {
                    WriteLine("      }");
                }
            }
        }

        //WriteLine("#endif");

        if (nd.Name != "SkippedTokensTriviaSyntax" &&
            nd.Name != "DocumentationCommentTriviaSyntax" &&
            nd.Name != "IncompleteMemberSyntax" &&
            valueFields.Count + nodeFields.Count <= 3)
        {
            //int hash;
            //var cached = SyntaxNodeCache.TryGetNode((int)SyntaxKind.IdentifierName, identifier, this.context, out hash);
            //if (cached != null) return (IdentifierNameSyntax)cached;

            //var result = new IdentifierNameSyntax(SyntaxKind.IdentifierName, identifier, this.context);
            //if (hash >= 0)
            //{
            //    SyntaxNodeCache.AddNode(result, hash);
            //}

            //return result;

            WriteLine();

            /* Remove
            //int hash;
            WriteLine("      int hash;");
            //SyntaxNode cached = SyntaxNodeCache.TryGetNode(SyntaxKind.IdentifierName, identifier, this.context, out hash);
            if (withSyntaxFactoryContext)
            {
                Write("      var cached = CSharpSyntaxNodeCache.TryGetNode((int)");
            }
            else
            {
                Write("      var cached = SyntaxNodeCache.TryGetNode((int)");
            }

            WriteCtorArgList(nd, withSyntaxFactoryContext, valueFields, nodeFields);
            WriteLine(", out hash);");
            //    if (cached != null) return (IdentifierNameSyntax)cached;
            WriteLine("      if (cached != null) return ({0})cached;", nd.Name);
            WriteLine(); */

            //var result = new IdentifierNameSyntax(SyntaxKind.IdentifierName, identifier);
            Write("      var result = new {0}(", nd.Name);
            WriteCtorArgList(nd, withSyntaxFactoryContext, valueFields, nodeFields);
            WriteLine(");");

            /* Remove
            //if (hash >= 0)
            WriteLine("      if (hash >= 0)");
            //{
            WriteLine("      {");
            //    SyntaxNodeCache.AddNode(result, hash);
            WriteLine("          SyntaxNodeCache.AddNode(result, hash);");
            //}
            WriteLine("      }"); */
            WriteLine();

            //return result;
            WriteLine("      return result;");
        }
        else
        {
            WriteLine();
            Write("      return new {0}(", nd.Name);
            WriteCtorArgList(nd, withSyntaxFactoryContext, valueFields, nodeFields);
            WriteLine(");");
        }

        WriteLine("    }");
    }

    private void WriteGreenFactoryParameters(Node nd)
    {
        if (nd.Kinds.Count > 1)
        {
            Write("SyntaxKind kind, ");
        }
        for (int i = 0, n = nd.Fields.Count; i < n; i++)
        {
            var field = nd.Fields[i];
            if (i > 0)
                Write(", ");
            var type = field.Type;
            if (type == "SyntaxNodeOrTokenList")
            {
                type = "SyntaxList<GreenNode>";
            }
            else if (IsSeparatedNodeList(field.Type) ||
                     IsNodeList(field.Type))
            {
                type = "Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax." + type;
            }
            Write("{0} {1}", type, CamelCase(field.Name));
        }
    }

    private void WriteCtorArgList(Node nd, bool withSyntaxFactoryContext, List<Field> valueFields, List<Field> nodeFields)
    {
        if (nd.Kinds.Count == 1)
        {
            Write("SyntaxKind.");
            Write(nd.Kinds[0].Name);
        }
        else
        {
            Write("kind");
        }
        for (int i = 0, n = nodeFields.Count; i < n; i++)
        {
            var field = nodeFields[i];
            Write(", ");
            if (field.Type == "SyntaxList<SyntaxToken>" || IsAnyList(field.Type))
            {
                Write("{0}.Node", CamelCase(field.Name));
            }
            else
            {
                Write(CamelCase(field.Name));
            }
        }
        // values are at end
        for (int i = 0, n = valueFields.Count; i < n; i++)
        {
            var field = valueFields[i];
            Write(", ");
            Write(UnderscoreCamelCase(field.Name));
        }
        if (withSyntaxFactoryContext)
        {
            Write(", this.context");
        }
    }

    private void WriteRedTypes()
    {
        var nodes = Tree.Types.Where(n => !(n is PredefinedNode)).ToList();
        for (int i = 0, n = nodes.Count; i < n; i++)
        {
            var node = nodes[i];
            WriteLine();
            WriteRedType(node);
        }
    }

    private List<Field> GetNodeOrNodeListFields(TreeType node)
        => node is AbstractNode an
            ? an.Fields.Where(n => IsNodeOrNodeList(n.Type)).ToList()
            : node is Node nd
                ? nd.Fields.Where(n => IsNodeOrNodeList(n.Type)).ToList()
                : new List<Field>();

    private void WriteRedType(TreeType node)
    {
        WriteComment(node.TypeComment, "  ");

        if (node is AbstractNode)
        {
            AbstractNode nd = (AbstractNode)node;
            WriteLine("  internal abstract partial class {0} : {1}", node.Name, node.Base);
            WriteLine("  {");
            WriteLine("    internal {0}(GreenNode green, SyntaxNode parent, int position)", node.Name);
            WriteLine("      : base(green, parent, position)");
            WriteLine("    {");
            WriteLine("    }");

            var valueFields = nd.Fields.Where(n => !IsNodeOrNodeList(n.Type)).ToList();
            var nodeFields = GetNodeOrNodeListFields(nd);

            for (int i = 0, n = nodeFields.Count; i < n; i++)
            {
                var field = nodeFields[i];
                if (IsNodeOrNodeList(field.Type))
                {
                    //red SyntaxLists can't contain tokens, so we switch to SyntaxTokenList
                    var fieldType = GetRedFieldType(field);
                    WriteLine();
                    WriteComment(field.PropertyComment, "    ");
                    WriteLine("    {0} abstract {1}{2} {3} {{ get; }}", "public", (IsNew(field) ? "new " : ""), fieldType, field.Name);
                    WriteLine($"    public {node.Name} With{field.Name}({fieldType} {CamelCase(field.Name)}) => With{field.Name}Core({CamelCase(field.Name)});");
                    WriteLine($"    internal abstract {node.Name} With{field.Name}Core({fieldType} {CamelCase(field.Name)});");

                    if (IsAnyList(field.Type))
                    {
                        var argType = GetElementType(field.Type);
                        WriteLine();
                        WriteLine("    public {0} Add{1}(params {2}[] items) => Add{1}Core(items);", node.Name, field.Name, argType);
                        WriteLine("    internal abstract {0} Add{1}Core(params {2}[] items);", node.Name, field.Name, argType);
                    }
                    else
                    {
                        var referencedNode = TryGetNodeForNestedList(field);
                        if (referencedNode != null)
                        {
                            for (int rf = 0; rf < referencedNode.Fields.Count; rf++)
                            {
                                var referencedNodeField = referencedNode.Fields[rf];
                                if (IsAnyList(referencedNodeField.Type))
                                {
                                    var argType = GetElementType(referencedNodeField.Type);

                                    WriteLine();
                                    WriteLine("    public {0} Add{1}{2}(params {3}[] items) => Add{1}{2}Core(items);", node.Name, StripPost(field.Name, "Opt"), referencedNodeField.Name, argType);
                                    WriteLine("    internal abstract {0} Add{1}{2}Core(params {3}[] items);", node.Name, StripPost(field.Name, "Opt"), referencedNodeField.Name, argType);
                                }
                            }
                        }
                    }
                }
            }

            for (int i = 0, n = valueFields.Count; i < n; i++)
            {
                var field = valueFields[i];
                WriteLine();
                WriteComment(field.PropertyComment, "    ");
                WriteLine("    {0} abstract {1}{2} {3} {{ get; }}", "public", (IsNew(field) ? "new " : ""), field.Type, field.Name);
            }

            var baseType = GetTreeType(node.Base);
            if (baseType != null)
            {
                var baseNodeFields = GetNodeOrNodeListFields(baseType);
                if (baseNodeFields.Count > 0)
                {
                    WriteLine();
                }

                foreach (var baseField in baseNodeFields)
                {
                    WriteLine($"    public new {node.Name} With{baseField.Name}({GetRedFieldType(baseField)} {CamelCase(baseField.Name)}) => ({node.Name})With{baseField.Name}Core({CamelCase(baseField.Name)});");
                }

                foreach (var baseField in baseNodeFields)
                {
                    if (IsAnyList(baseField.Type))
                    {
                        var argType = GetElementType(baseField.Type);
                        WriteLine();
                        WriteLine("    public new {0} Add{1}(params {2}[] items) => ({0})Add{1}Core(items);", node.Name, baseField.Name, argType);
                    }
                    else
                    {
                        var referencedNode = TryGetNodeForNestedList(baseField);
                        if (referencedNode != null)
                        {
                            // look for list members...
                            for (int rf = 0; rf < referencedNode.Fields.Count; rf++)
                            {
                                var referencedNodeField = referencedNode.Fields[rf];
                                if (IsAnyList(referencedNodeField.Type))
                                {
                                    var argType = GetElementType(referencedNodeField.Type);

                                    WriteLine();
                                    WriteLine("    public new {0} Add{1}{2}(params {3}[] items) => Add{1}{2}Core(items);", baseType.Name, StripPost(baseField.Name, "Opt"), referencedNodeField.Name, argType);
                                }
                            }
                        }
                    }
                }
            }

            WriteLine("  }");
        }
        else if (node is Node)
        {
            Node nd = (Node)node;
            WriteLine("  internal sealed partial class {0} : {1}", node.Name, node.Base);
            WriteLine("  {");

            var valueFields = nd.Fields.Where(n => !IsNodeOrNodeList(n.Type)).ToList();
            var nodeFields = nd.Fields.Where(n => IsNodeOrNodeList(n.Type)).ToList();

            for (int i = 0, n = nodeFields.Count; i < n; i++)
            {
                var field = nodeFields[i];
                //if (field.Type != "SyntaxToken"
                //    && field.Type != "SyntaxList<SyntaxToken>"
                //    )
                //{
                if (IsSeparatedNodeList(field.Type) || field.Type == "SyntaxNodeOrTokenList")
                {
                    WriteLine("    private SyntaxNode {0};", UnderscoreCamelCase(field.Name));
                }
                else
                {
                    var type = GetFieldType(field, green: false);
                    WriteLine("    private {0} {1};", type, UnderscoreCamelCase(field.Name));
                }
                //}
            }

            // write constructor
            WriteLine();
            WriteLine("    internal {0}(GreenNode green, SyntaxNode parent, int position)", node.Name);
            WriteLine("        : base(green, parent, position)");
            WriteLine("    {");
            WriteLine("    }");
            WriteLine();

            // property accessors
            for (int i = 0, n = nodeFields.Count; i < n; i++)
            {
                var field = nodeFields[i];
                //if (field.Type == "SyntaxToken")
                //{
                //    WriteComment(field.PropertyComment, "    ");
                //    WriteLine("    {0} {1}{2} {3} ", "public", OverrideOrNewModifier(field), field.Type, field.Name);
                //    WriteLine("    {");
                //    if (IsOptional(field))
                //    {
                //        WriteLine("        get");
                //        WriteLine("        {");
                //        WriteLine("            var slot = ((InternalSyntax.{0})Green).{1};", node.Name, field.Name);
                //        WriteLine("            if (slot != null)");
                //        WriteLine("                return new SyntaxToken(slot, this, {0});", GetChildPosition(i)/*, GetChildIndex(i)*/);
                //        WriteLine();
                //        WriteLine("            return default(SyntaxToken);");
                //        WriteLine("        }");
                //    }
                //    else
                //    {
                //        WriteLine("      get {{ return new SyntaxToken(((InternalSyntax.{0})Green).{1}, this, {2}); }}", node.Name, field.Name, GetChildPosition(i)/*, GetChildIndex(i)*/);
                //    }
                //    WriteLine("    }");
                //}
                /* Remove
                else if (field.Type == "SyntaxList<SyntaxToken>")
                {
                    WriteComment(field.PropertyComment, "    ");
                    WriteLine("    {0} {1}SyntaxTokenList {2} ", "public", OverrideOrNewModifier(field), field.Name);
                    WriteLine("    {");
                    WriteLine("        get");
                    WriteLine("        {");
                    WriteLine("            var slot = Green.GetSlot({0});", i);
                    WriteLine("            if (slot != null)");
                    WriteLine("                return new SyntaxTokenList(this, slot, {0}, {1});", GetChildPosition(i), GetChildIndex(i));
                    WriteLine();
                    WriteLine("            return default(SyntaxTokenList);");
                    WriteLine("        }");
                    WriteLine("    }");
                } */
                //else
                //{
                WriteComment(field.PropertyComment, "    ");
                WriteLine("    {0} {1}{2} {3} ", "public", OverrideOrNewModifier(field), field.Type, field.Name);
                WriteLine("    {");
                WriteLine("        get");
                WriteLine("        {");

                if (IsNodeList(field.Type))
                {
                    WriteLine("            return new {0}(GetRed(ref {1}, {2}));", field.Type, UnderscoreCamelCase(field.Name), i);
                }
                else if (IsSeparatedNodeList(field.Type))
                {
                    WriteLine("            var red = GetRed(ref {0}, {1});", UnderscoreCamelCase(field.Name), i);
                    WriteLine("            if (red != null)", i);
                    WriteLine("                return new {0}(red, {1});", field.Type, GetChildIndex(i));
                    WriteLine();
                    WriteLine("            return default({0});", field.Type);
                }
                else if (field.Type == "SyntaxNodeOrTokenList")
                {
                    throw new InvalidOperationException("field cannot be a random SyntaxNodeOrTokenList");
                }
                else
                {
                    if (i == 0)
                    {
                        WriteLine("            return GetRedAtZero(ref {0});", UnderscoreCamelCase(field.Name));
                    }
                    else
                    {
                        WriteLine("            return GetRed(ref {0}, {1});", UnderscoreCamelCase(field.Name), i);
                    }
                }
                WriteLine("        }");
                WriteLine("    }");
                //}
                WriteLine();
            }

            for (int i = 0, n = valueFields.Count; i < n; i++)
            {
                var field = valueFields[i];
                WriteComment(field.PropertyComment, "    ");
                WriteLine("    {0} {1}{2} {3} {{ get {{ return ((InternalSyntax.{4})Green).{3}; }} }}",
                    "public", OverrideOrNewModifier(field), field.Type, field.Name, node.Name
                    );
                WriteLine();
            }

            //GetNodeSlot forces creation of a red node.
            WriteLine("    internal override SyntaxNode GetNodeSlot(int index)");
            WriteLine("    {");
            WriteLine("        switch (index)");
            WriteLine("        {");
            for (int i = 0, n = nodeFields.Count; i < n; i++)
            {
                var field = nodeFields[i];

                //if (field.Type != "SyntaxToken" && field.Type != "SyntaxList<SyntaxToken>")
                if (true)
                {
                    if (i == 0)
                    {
                        WriteLine("            case {0}: return GetRedAtZero(ref {1});", i, UnderscoreCamelCase(field.Name));
                    }
                    else
                    {
                        WriteLine("            case {0}: return GetRed(ref {1}, {0});", i, UnderscoreCamelCase(field.Name));
                    }
                }
            }
            WriteLine("            default: return null;");
            WriteLine("        }");
            WriteLine("    }");

            //GetCachedSlot returns a red node if we have it.
            WriteLine("    internal override SyntaxNode GetCachedSlot(int index)");
            WriteLine("    {");
            WriteLine("        switch (index)");
            WriteLine("        {");
            for (int i = 0, n = nodeFields.Count; i < n; i++)
            {
                var field = nodeFields[i];
                //if (field.Type != "SyntaxToken" && field.Type != "SyntaxList<SyntaxToken>")
                if (true)
                {
                    WriteLine("            case {0}: return {1};", i, UnderscoreCamelCase(field.Name));
                }
            }
            WriteLine("            default: return null;");
            WriteLine("        }");
            WriteLine("    }");


            WriteRedAcceptMethods(nd);
            WriteRedUpdateMethod(nd);
            WriteRedWithMethods(nd);
            WriteRedListHelperMethods(nd);

            WriteLine("  }");
        }
    }

    private static string GetRedFieldType(Field field)
    {
        //return field.Type == "SyntaxList<SyntaxToken>" ? "SyntaxTokenList" : field.Type;
        return field.Type;
    }

    private string GetChildPosition(int i)
    {
        if (i == 0)
        {
            return "Position";
        }
        else
        {
            return "GetChildPosition(" + i + ")";
        }
    }

    private string GetChildIndex(int i)
    {
        if (i == 0)
        {
            return "0";
        }
        else
        {
            return "GetChildIndex(" + i + ")";
        }
    }

    private void WriteRedAcceptMethods(Node node)
    {
        //WriteRedAcceptMethod(node, true, true);
        WriteRedAcceptMethod(node, false, true);
        WriteRedAcceptMethod(node, false, false);
    }

    private void WriteRedAcceptMethod(Node node, bool genericArgument, bool genericResult)
    {
        string genericArgs =
            (genericResult && genericArgument) ? "<TArgument, TResult>" :
            genericResult ? "<TResult>" : "";
        WriteLine();
        WriteLine("    public override " + (genericResult ? "TResult" : "void") + " Accept" + genericArgs + "(SyntaxVisitor" + genericArgs + " visitor{0})", genericArgument ? ", TArgument argument" : "");
        WriteLine("    {");
        WriteLine("        " + (genericResult ? "return " : "") + "visitor.Visit{0}(this{1});", StripPost(node.Name, "Syntax"), genericArgument ? ", argument" : "");
        WriteLine("    }");
    }

    private void WriteRedVisitors()
    {
        //WriteRedVisitor(true, true);
        WriteRedVisitor(false, true);
        WriteRedVisitor(false, false);
    }

    private void WriteRedVisitor(bool genericArgument, bool genericResult)
    {
        string genericArgs =
            (genericResult && genericArgument) ? "<TArgument, TResult>" :
            genericResult ? "<TResult>" : "";
        var nodes = Tree.Types.Where(n => !(n is PredefinedNode)).ToList();

        WriteLine();
        WriteLine("  internal partial class SyntaxVisitor" + genericArgs);
        WriteLine("  {");
        int nWritten = 0;
        for (int i = 0, n = nodes.Count; i < n; i++)
        {
            if (nodes[i] is Node node)
            {
                if (nWritten > 0)
                    WriteLine();
                nWritten++;
                WriteComment(string.Format(CultureInfo.InvariantCulture, "<summary>Called when the visitor visits a {0} node.</summary>", node.Name), "    ");
                WriteLine("    public virtual " + (genericResult ? "TResult" : "void") + " Visit{0}({1} node{2})", StripPost(node.Name, "Syntax"), node.Name, genericArgument ? ", TArgument argument" : "");
                WriteLine("    {");
                WriteLine("      " + (genericResult ? "return " : "") + "DefaultVisit(node{0});", genericArgument ? ", argument" : "");
                WriteLine("    }");
            }
        }
        WriteLine("  }");
    }

    private void WriteRedUpdateMethod(Node node)
    {
        WriteLine();
        Write("    {0} {1} Update(", "public", node.Name);

        // parameters
        for (int f = 0; f < node.Fields.Count; f++)
        {
            var field = node.Fields[f];
            if (f > 0)
                Write(", ");
            //var type = field.Type == "SyntaxList<SyntaxToken>" ? "SyntaxTokenList" : field.Type;
            var type = field.Type;
            Write("{0} {1}", type, CamelCase(field.Name));
        }
        WriteLine(")");
        WriteLine("    {");

        Write("        if (");
        int nCompared = 0;
        for (int f = 0; f < node.Fields.Count; f++)
        {
            var field = node.Fields[f];
            if (IsDerivedOrListOfDerived("SyntaxNode", field.Type) || IsDerivedOrListOfDerived("SyntaxToken", field.Type) || field.Type == "SyntaxNodeOrTokenList")
            {
                if (nCompared > 0)
                    Write(" || ");
                Write("{0} != {1}", CamelCase(field.Name), field.Name);
                nCompared++;
            }
        }
        if (nCompared > 0)
        {
            WriteLine(")");
            WriteLine("        {");
            Write("            var newNode = SyntaxFactory.{0}(", StripPost(node.Name, "Syntax"));
            if (node.Kinds.Count > 1)
            {
                Write("Kind(), ");
            }
            for (int f = 0; f < node.Fields.Count; f++)
            {
                var field = node.Fields[f];
                if (f > 0)
                    Write(", ");
                Write(CamelCase(field.Name));
            }
            WriteLine(");");
            WriteLine("            var diagnostics = GetDiagnostics();");
            WriteLine("            if (diagnostics != null && diagnostics.Length > 0)");
            WriteLine("               newNode = newNode.WithDiagnostics(diagnostics);");
            WriteLine("            var annotations = GetAnnotations();");
            WriteLine("            if (annotations != null && annotations.Length > 0)");
            WriteLine("               return newNode.WithAnnotations(annotations);");
            WriteLine("            return newNode;");
            WriteLine("        }");
        }

        WriteLine();
        WriteLine("        return this;");
        WriteLine("    }");
    }

    private void WriteRedWithMethod(Node node)
    {
        WriteLine();
        Write("    public {0} With(", node.Name);

        // parameters
        for (int f = 0; f < node.Fields.Count; f++)
        {
            var field = node.Fields[f];
            var type = GetRedPropertyType(field);
            Write("Optional<{0}> {1} = default(Optional<{0}>)", type, UnderscoreCamelCase(field.Name));
            if (f < node.Fields.Count - 1)
                Write(", ");
        }
        WriteLine(")");
        WriteLine("    {");

        Write("        return Update(");

        for (int f = 0; f < node.Fields.Count; f++)
        {
            var field = node.Fields[f];
            var parameterName = UnderscoreCamelCase(field.Name);
            WriteLine();
            Write("                    {0}.HasValue ? {0}.Value : {1}", parameterName, field.Name);
            if (f < node.Fields.Count - 1)
                Write(",");
        }

        WriteLine();
        WriteLine("                    );");

        WriteLine("    }");
    }

    private void WriteRedWithMethods(Node node)
    {
        for (int f = 0; f < node.Fields.Count; f++)
        {
            var field = node.Fields[f];
            var type = GetRedPropertyType(field);

            WriteLine();

            var isNew = false;
            if (IsOverride(field))
            {
                var baseType = GetHighestBaseTypeWithField(node, field.Name);
                if (baseType != null)
                {
                    WriteLine($"    internal override {baseType.Name} With{field.Name}Core({type} {CamelCase(field.Name)}) => With{field.Name}({CamelCase(field.Name)});");
                    isNew = true;
                }
            }

            WriteLine($"    public{(isNew ? " new " : " ")}{node.Name} With{StripPost(field.Name, "Opt")}({type} {CamelCase(field.Name)})");
            WriteLine("    {");

            // call update inside each setter
            Write("        return Update(");
            for (int f2 = 0; f2 < node.Fields.Count; f2++)
            {
                var field2 = node.Fields[f2];
                if (f2 > 0)
                    Write(", ");

                if (field2 == field)
                {
                    Write("{0}", CamelCase(field2.Name));
                }
                else
                {
                    Write("{0}", field2.Name);
                }
            }
            WriteLine(");");

            WriteLine("    }");
        }
    }

    private TreeType GetHighestBaseTypeWithField(TreeType node, string name)
    {
        TreeType bestType = null;
        for (var current = node; current != null; current = TryGetBaseType(current))
        {
            var fields = GetNodeOrNodeListFields(current);
            var field = fields.FirstOrDefault(f => f.Name == name);
            if (field != null)
            {
                bestType = current;
            }
        }

        return bestType;
    }

    private TreeType TryGetBaseType(TreeType node)
        => node is AbstractNode an
            ? GetTreeType(an.Base)
            : node is Node n
                ? GetTreeType(n.Base)
                : null;

    private void WriteRedListHelperMethods(Node node)
    {
        for (int f = 0; f < node.Fields.Count; f++)
        {
            var field = node.Fields[f];

            if (IsAnyList(field.Type))
            {
                // write list helper methods for list properties
                WriteRedListHelperMethods(node, field);
            }
            else
            {
                var referencedNode = TryGetNodeForNestedList(field);
                if (referencedNode != null)
                {
                    // look for list members...
                    for (int rf = 0; rf < referencedNode.Fields.Count; rf++)
                    {
                        var referencedNodeField = referencedNode.Fields[rf];
                        if (IsAnyList(referencedNodeField.Type))
                        {
                            WriteRedNestedListHelperMethods(node, field, referencedNode, referencedNodeField);
                        }
                    }
                }
            }
        }
    }

    private Node TryGetNodeForNestedList(Field field)
    {
        Node referencedNode = GetNode(field.Type);
        if (referencedNode != null && (!IsOptional(field) || RequiredFactoryArgumentCount(referencedNode) == 0))
        {
            return referencedNode;
        }

        return null;
    }

    private void WriteRedListHelperMethods(Node node, Field field)
    {
        var argType = GetElementType(field.Type);

        var isNew = false;
        if (IsOverride(field))
        {
            var baseType = GetHighestBaseTypeWithField(node, field.Name);
            if (baseType != null)
            {
                WriteLine("    internal override {0} Add{1}Core(params {2}[] items) => Add{1}(items);", baseType.Name, field.Name, argType);
                isNew = true;
            }
        }

        WriteLine();
        WriteLine($"    public{(isNew ? " new " : " ")}{node.Name} Add{field.Name}(params {argType}[] items)");
        WriteLine("    {");
        WriteLine("        return With{0}(this.{1}.AddRange(items));", StripPost(field.Name, "Opt"), field.Name);
        WriteLine("    }");
    }

    private void WriteRedNestedListHelperMethods(Node node, Field field, Node referencedNode, Field referencedNodeField)
    {
        var argType = GetElementType(referencedNodeField.Type);

        var isNew = false;
        if (IsOverride(field))
        {
            var baseType = GetHighestBaseTypeWithField(node, field.Name);
            if (baseType != null)
            {
                WriteLine("    internal override {0} Add{1}{2}Core(params {3}[] items) => Add{1}{2}(items);", baseType.Name, StripPost(field.Name, "Opt"), referencedNodeField.Name, argType);
                isNew = true;
            }
        }

        // AddBaseListTypes
        WriteLine();
        WriteLine($"    public{(isNew ? " new " : " ")}{node.Name} Add{StripPost(field.Name, "Opt")}{referencedNodeField.Name}(params {argType}[] items)");
        WriteLine("    {");

        if (IsOptional(field))
        {
            var factoryName = StripPost(referencedNode.Name, "Syntax");
            var varName = StripPost(UnderscoreCamelCase(field.Name), "Opt");
            WriteLine("        var {0} = this.{1} ?? SyntaxFactory.{2}();", varName, field.Name, factoryName);
            WriteLine("        return this.With{0}({1}.With{2}({1}.{3}.AddRange(items)));", StripPost(field.Name, "Opt"), varName, StripPost(referencedNodeField.Name, "Opt"), referencedNodeField.Name);
        }
        else
        {
            WriteLine("        return this.With{0}(this.{1}.With{2}(this.{1}.{3}.AddRange(items)));", StripPost(field.Name, "Opt"), field.Name, StripPost(referencedNodeField.Name, "Opt"), referencedNodeField.Name);
        }

        WriteLine("    }");
    }

    private void WriteRedRewriter()
    {
        var nodes = Tree.Types.Where(n => !(n is PredefinedNode)).ToList();

        WriteLine();
        WriteLine("  internal partial class SyntaxRewriter : SyntaxVisitor<SyntaxNode>");
        WriteLine("  {");
        int nWritten = 0;
        for (int i = 0, n = nodes.Count; i < n; i++)
        {
            if (nodes[i] is Node node)
            {
                var nodeFields = node.Fields.Where(nd => IsNodeOrNodeList(nd.Type)).ToList();

                if (nWritten > 0)
                    WriteLine();
                nWritten++;
                WriteLine("    public override SyntaxNode Visit{0}({1} node)", StripPost(node.Name, "Syntax"), node.Name);
                WriteLine("    {");
                for (int f = 0; f < nodeFields.Count; f++)
                {
                    var field = nodeFields[f];
                    if (IsAnyList(field.Type))
                    {
                        WriteLine("      var {0} = VisitList(node.{1});", CamelCase(field.Name), field.Name);
                    }
                    else if (field.Type == "SyntaxToken")
                    {
                        WriteLine("      var {0} = ({1})VisitToken(node.{2});", CamelCase(field.Name), field.Type, field.Name);
                    }
                    else
                    {
                        WriteLine("      var {0} = ({1})Visit(node.{2});", CamelCase(field.Name), field.Type, field.Name);
                    }
                }
                if (nodeFields.Count > 0)
                {
                    Write("      return node.Update(");
                    for (int f = 0; f < node.Fields.Count; f++)
                    {
                        var field = node.Fields[f];
                        if (f > 0)
                            Write(", ");
                        if (IsNodeOrNodeList(field.Type))
                        {
                            Write(CamelCase(field.Name));
                        }
                        else
                        {
                            Write("node.{0}", field.Name);
                        }
                    }
                    WriteLine(");");
                }
                else
                {
                    WriteLine("      return node;");
                }
                WriteLine("    }");
            }
        }
        WriteLine("  }");
    }

    private void WriteRedFactories()
    {
        var nodes = Tree.Types.Where(n => !(n is PredefinedNode) && !(n is AbstractNode)).OfType<Node>().ToList();
        WriteLine();
        WriteLine("  internal static partial class SyntaxFactory");
        WriteLine("  {");

        for (int i = 0, n = nodes.Count; i < n; i++)
        {
            var node = nodes[i];
            WriteRedFactory(node);
            WriteRedFactoryWithNoAutoCreatableTokens(node);
            WriteRedMinimalFactory(node);
            WriteRedMinimalFactory(node, withStringNames: true);
            WriteKindConverters(node);
        }

        WriteLine("  }");
    }

    protected bool CanBeAutoCreated(Node node, Field field)
    {
        return IsAutoCreatableToken(node, field) || IsAutoCreatableNode(node, field);
    }

    private bool IsAutoCreatableToken(Node node, Field field)
    {
        return field.Type == "SyntaxToken"
            && field.Kinds != null
            && ((field.Kinds.Count == 1 && field.Kinds[0].Name != "IdentifierToken" && !field.Kinds[0].Name.EndsWith("LiteralToken", StringComparison.Ordinal)) || (field.Kinds.Count > 1 && field.Kinds.Count == node.Kinds.Count));
    }

    private bool IsAutoCreatableNode(Node node, Field field)
    {
        var referencedNode = GetNode(field.Type);
        return (referencedNode != null && RequiredFactoryArgumentCount(referencedNode) == 0);
    }

    private bool IsRequiredFactoryField(Node node, Field field)
    {
        return (!IsOptional(field) && !IsAnyList(field.Type) && !CanBeAutoCreated(node, field)) || IsValueField(field);
    }

    private bool IsValueField(Field field)
    {
        return !IsNodeOrNodeList(field.Type);
    }

    private int RequiredFactoryArgumentCount(Node nd, bool includeKind = true)
    {
        int count = 0;

        // kind must be specified in factory
        if (nd.Kinds.Count > 1 && includeKind)
        {
            count++;
        }

        for (int i = 0, n = nd.Fields.Count; i < n; i++)
        {
            var field = nd.Fields[i];
            if (IsRequiredFactoryField(nd, field))
            {
                count++;
            }
        }

        return count;
    }

    private int OptionalFactoryArgumentCount(Node nd)
    {
        int count = 0;
        for (int i = 0, n = nd.Fields.Count; i < n; i++)
        {
            var field = nd.Fields[i];
            if (IsOptional(field) || CanBeAutoCreated(nd, field) || IsAnyList(field.Type))
            {
                count++;
            }
        }

        return count;
    }

    // full factory signature with nothing optional
    private void WriteRedFactory(Node nd)
    {
        WriteLine();

        var valueFields = nd.Fields.Where(n => IsValueField(n)).ToList();
        var nodeFields = nd.Fields.Where(n => !IsValueField(n)).ToList();

        WriteComment(string.Format(CultureInfo.InvariantCulture, "<summary>Creates a new {0} instance.</summary>", nd.Name), "    ");

        Write("    {0} static {1} {2}(", "public", nd.Name, StripPost(nd.Name, "Syntax"));
        WriteRedFactoryParameters(nd);

        WriteLine(")");
        WriteLine("    {");

        // validate kinds
        if (nd.Kinds.Count > 1)
        {
            WriteLine("      switch (kind)");
            WriteLine("      {");
            foreach (var kind in nd.Kinds)
            {
                WriteLine("        case SyntaxKind.{0}:", kind.Name);
            }
            WriteLine("          break;");
            WriteLine("        default:");
            WriteLine("          throw new ArgumentException(\"kind\");");
            WriteLine("      }");
        }

        // validate parameters
        for (int i = 0, n = nodeFields.Count; i < n; i++)
        {
            var field = nodeFields[i];
            var pname = CamelCase(field.Name);

            if (field.Type == "SyntaxToken")
            {
                if (field.Kinds != null && field.Kinds.Count > 0)
                {
                    if (IsOptional(field))
                    {
                        WriteLine("      if ({0} != null)", CamelCase(field.Name));
                        WriteLine("      {");
                    }
                    WriteLine("      switch ({0}.Kind)", pname);
                    WriteLine("      {");
                    foreach (var kind in field.Kinds)
                    {
                        WriteLine("        case SyntaxKind.{0}:", kind.Name);
                    }
                    if (IsOptional(field))
                    {
                        WriteLine("        case SyntaxKind.None:");
                    }
                    WriteLine("          break;");
                    WriteLine("        default:");
                    WriteLine("          throw new ArgumentException(\"{0}\");", pname);
                    WriteLine("      }");
                    if (IsOptional(field))
                    {
                        WriteLine("      }");
                    }
                }
            }
            else if (!IsAnyList(field.Type) && !IsOptional(field))
            {
                WriteLine("      if ({0} == null)", CamelCase(field.Name));
                WriteLine("        throw new ArgumentNullException(nameof({0}));", CamelCase(field.Name));
            }
        }

        Write("      return ({0})InternalSyntax.SyntaxFactory.{1}(", nd.Name, StripPost(nd.Name, "Syntax"));
        if (nd.Kinds.Count > 1)
        {
            Write("kind, ");
        }
        for (int i = 0, n = nodeFields.Count; i < n; i++)
        {
            var field = nodeFields[i];
            if (i > 0)
                Write(", ");
            if (field.Type == "SyntaxToken")
            {
                if (IsOptional(field))
                {
                    Write("(Syntax.InternalSyntax.SyntaxToken){0}?.Green", CamelCase(field.Name));
                }
                else
                {
                    Write("(Syntax.InternalSyntax.SyntaxToken){0}.Green", CamelCase(field.Name));
                }
            }
            else if (field.Type == "SyntaxList<SyntaxToken>")
            {
                Write("{0}.Node.ToGreenList<InternalSyntax.SyntaxToken>()", CamelCase(field.Name));
            }
            else if (IsNodeList(field.Type))
            {
                Write("{0}.Node.ToGreenList<InternalSyntax.{1}>()", CamelCase(field.Name), GetElementType(field.Type));
            }
            else if (IsSeparatedNodeList(field.Type))
            {
                Write("{0}.Node.ToGreenSeparatedList<InternalSyntax.{1}>()", CamelCase(field.Name), GetElementType(field.Type));
            }
            else if (field.Type == "SyntaxNodeOrTokenList")
            {
                Write("{0}.Node.ToGreenList<GreenNode>()", CamelCase(field.Name));
            }
            else
            {
                Write("{0} == null ? null : (InternalSyntax.{1}){0}.Green", CamelCase(field.Name), field.Type);
            }
        }

        // values are at end
        for (int i = 0, n = valueFields.Count; i < n; i++)
        {
            var field = valueFields[i];
            Write(", ");
            Write(CamelCase(field.Name));
        }

        WriteLine(").CreateRed();");
        WriteLine("    }");

        //WriteLine();
    }

    private void WriteRedFactoryParameters(Node nd)
    {
        if (nd.Kinds.Count > 1)
        {
            Write("SyntaxKind kind, ");
        }

        for (int i = 0, n = nd.Fields.Count; i < n; i++)
        {
            var field = nd.Fields[i];
            if (i > 0)
                Write(", ");
            var type = GetRedPropertyType(field);

            Write("{0} {1}", type, CamelCase(field.Name));
        }
    }

    private string GetRedPropertyType(Field field)
    {
        //if (field.Type == "SyntaxList<SyntaxToken>")
        //    return "SyntaxTokenList";
        return field.Type;
    }

    private string GetDefaultValue(Node nd, Field field)
    {
        if (IsRequiredFactoryField(nd, field))
        {
            Console.WriteLine(nd.Name);
            Console.WriteLine(field.Name);
        }
        System.Diagnostics.Debug.Assert(!IsRequiredFactoryField(nd, field));

        if (IsOptional(field) || IsAnyList(field.Type))
        {
            return string.Format(CultureInfo.InvariantCulture, "default({0})", GetRedPropertyType(field));
        }
        else if (field.Type == "SyntaxToken")
        {
            // auto construct token?
            if (field.Kinds.Count == 1)
            {
                return string.Format(CultureInfo.InvariantCulture, "SyntaxFactory.Token(SyntaxKind.{0})", field.Kinds[0].Name);
            }
            else
            {
                return string.Format(CultureInfo.InvariantCulture, "SyntaxFactory.Token(Get{0}{1}Kind(kind))", StripPost(nd.Name, "Syntax"), StripPost(field.Name, "Opt"));
            }
        }
        else
        {
            var referencedNode = GetNode(field.Type);
            return string.Format(CultureInfo.InvariantCulture, "SyntaxFactory.{0}()", StripPost(referencedNode.Name, "Syntax"));
        }
    }

    // Writes Get<Property>Kind() methods for converting between node kind and member token kinds...
    private void WriteKindConverters(Node nd)
    {
        for (int f = 0; f < nd.Fields.Count; f++)
        {
            var field = nd.Fields[f];

            if (field.Type == "SyntaxToken" && CanBeAutoCreated(nd, field) && field.Kinds.Count > 1)
            {
                WriteLine();
                WriteLine("    private static SyntaxKind Get{0}{1}Kind(SyntaxKind kind)", StripPost(nd.Name, "Syntax"), StripPost(field.Name, "Opt"));
                WriteLine("    {");

                WriteLine("      switch (kind)");
                WriteLine("      {");

                for (int k = 0; k < field.Kinds.Count; k++)
                {
                    var nKind = nd.Kinds[k];
                    var pKind = field.Kinds[k];
                    WriteLine("        case SyntaxKind.{0}:", nKind.Name);
                    WriteLine("          return SyntaxKind.{0};", pKind.Name);
                }

                WriteLine("        default:");
                WriteLine("          throw new ArgumentOutOfRangeException();");
                WriteLine("      }");
                WriteLine("    }");
            }
        }
    }

    private IEnumerable<Field> DetermineRedFactoryWithNoAutoCreatableTokenFields(Node nd)
    {
        return nd.Fields.Where(f => !IsAutoCreatableToken(nd, f));
    }

    // creates a factory without auto-creatable token arguments
    private void WriteRedFactoryWithNoAutoCreatableTokens(Node nd)
    {
        var nAutoCreatableTokens = nd.Fields.Count(f => IsAutoCreatableToken(nd, f));
        if (nAutoCreatableTokens == 0)
            return; // already handled by general factory

        var factoryWithNoAutoCreatableTokenFields = new HashSet<Field>(DetermineRedFactoryWithNoAutoCreatableTokenFields(nd));
        var minimalFactoryFields = DetermineMinimalFactoryFields(nd);
        if (minimalFactoryFields != null && factoryWithNoAutoCreatableTokenFields.SetEquals(minimalFactoryFields))
        {
            return; // will be handled in minimal factory case
        }

        WriteLine();

        WriteComment(string.Format(CultureInfo.InvariantCulture, "<summary>Creates a new {0} instance.</summary>", nd.Name), "    ");
        Write("    {0} static {1} {2}(", "public", nd.Name, StripPost(nd.Name, "Syntax"));

        bool hasPreviousParameter = false;
        if (nd.Kinds.Count > 1)
        {
            Write("SyntaxKind kind");
            hasPreviousParameter = true;
        }

        for (int i = 0, n = nd.Fields.Count; i < n; i++)
        {
            var field = nd.Fields[i];

            if (factoryWithNoAutoCreatableTokenFields.Contains(field))
            {
                if (hasPreviousParameter)
                    Write(", ");

                Write("{0} {1}", GetRedPropertyType(field), CamelCase(field.Name));

                hasPreviousParameter = true;
            }
        }
        WriteLine(")");

        WriteLine("    {");

        Write("      return SyntaxFactory.{0}(", StripPost(nd.Name, "Syntax"));

        bool hasPreviousArgument = false;
        if (nd.Kinds.Count > 1)
        {
            Write("kind");
            hasPreviousArgument = true;
        }

        for (int i = 0, n = nd.Fields.Count; i < n; i++)
        {
            var field = nd.Fields[i];

            if (hasPreviousArgument)
                Write(", ");

            if (factoryWithNoAutoCreatableTokenFields.Contains(field))
            {
                // pass supplied parameter on to general factory
                Write("{0}", CamelCase(field.Name));
            }
            else
            {
                // pass an auto-created token to the general factory
                Write("{0}", GetDefaultValue(nd, field));
            }

            hasPreviousArgument = true;
        }

        WriteLine(");");

        WriteLine("    }");
    }

    private Field DetermineMinimalOptionalField(Node nd)
    {
        // first if there is a single list, then choose the list because it would not have been optional
        int listCount = nd.Fields.Count(f => IsAnyNodeList(f.Type));
        if (listCount == 1)
        {
            return nd.Fields.First(f => IsAnyNodeList(f.Type));
        }
        else
        {
            // otherwise, if there is a single optional node, use that..
            int nodeCount = nd.Fields.Count(f => IsNode(f.Type) && f.Type != "SyntaxToken");
            if (nodeCount == 1)
            {
                return nd.Fields.First(f => IsNode(f.Type) && f.Type != "SyntaxToken");
            }
            else
            {
                return null;
            }
        }
    }

    private IEnumerable<Field> DetermineMinimalFactoryFields(Node nd)
    {
        // special case to allow a single optional argument if there would have been no arguments
        // and we can determine a best single argument.
        Field allowOptionalField = null;

        var optionalCount = OptionalFactoryArgumentCount(nd);
        if (optionalCount == 0)
        {
            return null; // no fields...
        }

        var requiredCount = RequiredFactoryArgumentCount(nd, includeKind: false);
        if (requiredCount == 0 && optionalCount > 1)
        {
            allowOptionalField = DetermineMinimalOptionalField(nd);
        }

        return nd.Fields.Where(f => IsRequiredFactoryField(nd, f) || allowOptionalField == f);
    }

    // creates a factory with only the required arguments (everything else is defaulted)
    private void WriteRedMinimalFactory(Node nd, bool withStringNames = false)
    {
        var optionalCount = OptionalFactoryArgumentCount(nd);
        if (optionalCount == 0)
            return; // already handled w/ general factory method

        var minimalFactoryfields = new HashSet<Field>(DetermineMinimalFactoryFields(nd));

        if (withStringNames && !minimalFactoryfields.Any(f => IsRequiredFactoryField(nd, f) && CanAutoConvertFromString(f)))
            return; // no string-name overload necessary

        WriteLine();

        WriteComment(string.Format(CultureInfo.InvariantCulture, "<summary>Creates a new {0} instance.</summary>", nd.Name), "    ");
        Write("    {0} static {1} {2}(", "public", nd.Name, StripPost(nd.Name, "Syntax"));

        bool hasPreviousParameter = false;
        if (nd.Kinds.Count > 1)
        {
            Write("SyntaxKind kind");
            hasPreviousParameter = true;
        }

        for (int i = 0, n = nd.Fields.Count; i < n; i++)
        {
            var field = nd.Fields[i];

            if (minimalFactoryfields.Contains(field))
            {
                var type = GetRedPropertyType(field);

                if (IsRequiredFactoryField(nd, field))
                {
                    if (hasPreviousParameter)
                        Write(", ");

                    if (withStringNames && CanAutoConvertFromString(field))
                    {
                        type = "string";
                    }

                    Write("{0} {1}", type, CamelCase(field.Name));

                    hasPreviousParameter = true;
                }
                else
                {
                    if (hasPreviousParameter)
                        Write(", ");

                    Write("{0} {1} = default({0})", type, CamelCase(field.Name));

                    hasPreviousParameter = true;
                }
            }
        }
        WriteLine(")");

        WriteLine("    {");

        Write("      return SyntaxFactory.{0}(", StripPost(nd.Name, "Syntax"));

        bool hasPreviousArgument = false;
        if (nd.Kinds.Count > 1)
        {
            Write("kind");
            hasPreviousArgument = true;
        }

        for (int i = 0, n = nd.Fields.Count; i < n; i++)
        {
            var field = nd.Fields[i];

            if (hasPreviousArgument)
                Write(", ");

            if (minimalFactoryfields.Contains(field))
            {
                if (IsRequiredFactoryField(nd, field))
                {
                    if (withStringNames && CanAutoConvertFromString(field))
                    {
                        Write("{0}({1})", GetStringConverterMethod(field), CamelCase(field.Name));
                    }
                    else
                    {
                        Write("{0}", CamelCase(field.Name));
                    }
                }
                else
                {
                    if (IsOptional(field) || IsAnyList(field.Type))
                    {
                        Write("{0}", CamelCase(field.Name));
                    }
                    else
                    {
                        Write("{0} ?? {1}", CamelCase(field.Name), GetDefaultValue(nd, field));
                    }
                }
            }
            else
            {
                var defaultValue = GetDefaultValue(nd, field);
                Write(defaultValue);
            }

            hasPreviousArgument = true;
        }

        WriteLine(");");

        WriteLine("    }");
    }

    private bool CanAutoConvertFromString(Field field)
    {
        return IsIdentifierToken(field) || IsIdentifierNameSyntax(field);
    }

    private bool IsIdentifierToken(Field field)
    {
        return field.Type == "SyntaxToken" && field.Kinds != null && field.Kinds.Count == 1 && field.Kinds[0].Name == "IdentifierToken";
    }

    private bool IsIdentifierNameSyntax(Field field)
    {
        return field.Type == "IdentifierNameSyntax";
    }

    private string GetStringConverterMethod(Field field)
    {
        if (IsIdentifierToken(field))
        {
            return "SyntaxFactory.Identifier";
        }
        else if (IsIdentifierNameSyntax(field))
        {
            return "SyntaxFactory.IdentifierName";
        }
        else
        {
            throw new NotSupportedException();
        }
    }

    /// <summary>
    /// Anything inside a &lt;Comment&gt; tag gets written out (escaping untouched) as the
    /// XML doc comment.  Line breaks will be preserved.
    /// </summary>
    private void WriteComment(string comment, string indent)
    {
        if (comment != null)
        {
            var lines = comment.Split(new string[] { "\r", "\n", "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
            foreach (var line in lines.Where(l => !string.IsNullOrWhiteSpace(l)))
            {
                WriteLine("{0}/// {1}", indent, line.TrimStart());
            }
        }
    }

    /// <summary>
    /// Anything inside a &lt;Comment&gt; tag gets written out (escaping untouched) as the
    /// XML doc comment.  Line breaks will be preserved.
    /// </summary>
    private void WriteComment(Comment comment, string indent)
    {
        if (comment != null)
        {
            foreach (XmlElement element in comment.Body)
            {
                string[] lines = element.OuterXml.Split(new string[] { "\r", "\n", "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
                foreach (string line in lines.Where(l => !string.IsNullOrWhiteSpace(l)))
                {
                    WriteLine("{0}/// {1}", indent, line.TrimStart());
                }
            }
        }
    }
}
